Вопрос, который мне задают чаще всего, — как разговаривать о рефакторинге с руководителем?
В таких случаях я даю несколько спорный совет: не говорите ему ничего!
Мартин Фаулер, «Рефакторинг. Улучшение существующего кода»
Устаревание кода, трудности с поддержкой, непредсказуемые баги — эти термины один за другим появляются в жизни разработчика по мере разработки продукта. И если первое — это скорее интересы разработчика, то последнее — это прямая проблема бизнеса.
В этой статье я хочу поделиться опытом переписывания большого проекта и как бонус привести пару кусков кода, которые помогли нам и, надеюсь, помогут вам начать этот интересный путь.
Разбор полетов
Проблемы
Они обычно начинаются по известному сценарию:
- Прибегает начальник с воплями «У нас ничего не работает, главный клиент под угрозой!»;
- или менеджер с просьбой прикрутить нереализуемую фишку;
- реже мы, разработчики, настолько устаем копаться в
говне«легаси»-коде, что решаем переписать всё.
И обычно это заканчивается всеобщим негодованием и разладом, потому что фишка нужна срочно, клиенты тоже ждать не могут, а из-за печального наследия команда стремится разбежаться. Ситуацию портит отсутствие «денег на рефакторинг» (бездействие команды в понятиях бизнеса)
Насчет последнего пункта нужно добавить, что ситуацию с новым человеком в команде, который рвется всё переписать, я не рассматриваю, однако он может запросто аргументировать описанный подход для развития проекта.
Задачи
- Перевести проект на современную архитектуру
- Обеспечить минимальные затраты на рефакторинг
Принципиальная схема реализации
Наш проект был изначально написан на Kohana, переписывали мы его на Symfony2, поэтому все примеры приведены в контексте этих систем. Однако, данный подход можно применять с любыми фреймворками. Необходимое требование — единая точка входа в приложение.
Изначально приложение обрабатывает запросы пользователя через точку входа «app_kohana.php»
Мы будем оборачивать начальную точку входа в новой системе, организовывая своеобразный «прокси».
Рефакторинг
Контроллер — обертка для старой системы
Идея довольно проста и заключается в следующем:
- Разворачиваем параллельно две системы (kohana + symfony)
- Меняем точку входа на новую (symfony)
- Организуем универсальный контроллер, который по умолчанию будет пробрасывать все запросы в старую систему
И если с первыми двумя пунктами проблем возникнуть не должно, то третий представляет интерес, потому как в нем могут обнаружиться подводные камни.
Первое, что приходит в голову — обернуть инклюд в ob_start. Так и сделаем:
class PassThroughController extends Symfony\Bundle\FrameworkBundle\Controller\Controller { public function kohanaAction() { ob_start(); $kohanaPath = $this->container->getParameter('kernel.root_dir') . '/../app_kohana.php'; include $kohanaPath; $response = ob_get_clean(); return new Response($response); } }
application.passthrough_kohana: path: /{slug} defaults: _controller: ApplicationBundle:PassThrough:kohana requirements: slug: .*
В таком формате система уже работает, но спустя какое-то время прилетает первый баг. Например, некорректная обработка ajax-ошибок. Или на сайте ошибки отдаются с кодом 200 вместо 404.
Тут мы понимаем, что буфер проглатывает заголовки, поэтому их нужно обрабатывать явным образом
class PassThroughController extends Symfony\Bundle\FrameworkBundle\Controller\Controller { public function kohanaAction() { ob_start(); $kohanaPath = $this->container->getParameter('kernel.root_dir') . '/../app_kohana.php'; include $kohanaPath; $headers = headers_list(); $code = http_response_code(); $response = ob_get_clean(); return new Response($response, $code, $headers); } }
После этого полёт нормальный.
Проблемы старой системы, влияющие на функционирование новой
exit()
У нас в системе нашлись места, где в конце работы контроллера радостно вызывался exit(). Это практикуется, например, в Yii (CApplication::end()). Особой головной боли это не доставляет до тех пор, пока не начинаешь использовать событийную модель в новой системе и обрабатывать события, случающиеся после выполнения контроллера. Самый яркий пример — Symfony Profiler, который прекращает работать для запросов с exit’ом.
Данный случай нужно иметь в виду и при необходимости предпринимать соответствующие меры.
ob_end_*()
Необдуманное использование функций ob_end легко может поломать работу новой системы, очистив буфер нового прокси-контроллера. Следует так же иметь в виду.
Kohana_Controller_Template::$auto_render
Переменная отвечает за автоматическую отрисовку полученных из контроллера данных в глобальном шаблоне (может сильно зависеть от используемого шаблонизатора). Во время адаптации новой системы это может сэкономить время на отладку в местах, где, например, json выводится простым echo $json; exit();. Контроллер примет примерно следующий вид:
$this->auto_render = false; echo $json; return;
О чем еще стоит позаботиться
Описанные выше точки входа — это идеальная ситуация. У нас изначально точка входа была app.php и требовалось, чтобы после рефакторинга она же и осталась (переконфигурирование многочисленных серверов выглядело бесперспективным). Выбран был следующий алгоритм:
- Переименовываем app.php в app_kohana.php
- Точку входа симфони размещаем в app.php
- Profit
И все, казалось бы, завелось, кроме консольных команд, которые в кохане запускались через тот же файл. Поэтому в начале нового app.php родился следующий костылик для обратной совместимости:
if (PHP_SAPI == 'cli') { include 'app_kohana.php'; return; }
Жизнь после рефакторинга
Новые контроллеры
Все новые контроллеры мы стараемся писать в symfony. Разделение происходит на уровне роутинга, перед «универсальным» маршрутом дописывается нужный, и Kohana дальше не загружается. Пока мы пишем в новой системе только ajax-контроллеры, поэтому вопрос с переиспользованием шаблонов (Twig) остается открытым.
БД и Конфигурация
Для доступа к БД были сгенерированы модели из текущей базы стандартными методами Doctrine. В репозитории по мере необходимости добавляются новые методы работы с БД. Однако, конфигурация подключения к БД используется существующая из коханы. Для этого написан конфигурационный файл, который подтягивает данные из конфига коханы и преобразует их в параметры конфигурации симфони. Логика поиска конфига в зависимости от платформы, увы, продублирована, чтобы не подключать классы коханы в новой системе.
/** @var \Symfony\Component\DependencyInjection\ContainerBuilder $container */ $kohanaDatabaseConfig = []; $kohanaConfigPath = $container->getParameter('kernel.root_dir') . '/config'; if (!defined('SYSPATH')) { define('SYSPATH', realpath($container->getParameter('kernel.root_dir') . '/../vendor/kohana/core') . '/'); } $mainConfig = $kohanaConfigPath . '/database.php'; if (file_exists($mainConfig)) { $kohanaDatabaseConfig = include $mainConfig; } if (isset($_SERVER['PLATFORM'])) { $kohanaEnvConfig = $kohanaConfigPath . '/' . $_SERVER['PLATFORM'] . '/database.php'; if (file_exists($kohanaEnvConfig)) { $kohanaDatabaseConfig = array_merge($kohanaDatabaseConfig, include $kohanaEnvConfig); } } if (empty($kohanaDatabaseConfig['default'])) { throw new \Symfony\Component\Filesystem\Exception\FileNotFoundException('Could not load database config'); } $dbParams = $kohanaDatabaseConfig['default']; $container->getParameterBag()->add([ 'database_driver' => 'pdo_mysql', 'database_host' => $dbParams['connection']['hostname'], 'database_port' => null, 'database_name' => $dbParams['connection']['database'], 'database_user' => $dbParams['connection']['username'], 'database_password' => $dbParams['connection']['password'], ]);
Подключается конфиг стандартным способом
class ApplicationExtension extends Symfony\Component\HttpKernel\DependencyInjection\Extension { public function load(array $configs, ContainerBuilder $container) { $loader = new Loader\PhpFileLoader($container, new FileLocator(__DIR__.'/../Resources/config')); $loader->load('kohana.php'); } }
Как продолжать: вынесение функционала в сервисы
Дальнейшей движение из коханы в симфони очень хорошо укладывается в вынесение функционала в сервисы симфони и использовании их в старой системе через DI-контейнер. Так сложилось, что DI-компонент мы начали использовать до подключения симфони в проект, поэтому этот процесс прошел довольно гладко, но ничто не мешает делать это с нуля. Основной задачей будет прокинуть DI-контейнер из симфони в кохану. Мы сделали это в кохана-стиле через статическое свойство, в другом фреймворке можно найти соответствующий подход.
class Kohana extends Kohana_Core { /** * @var Symfony\Component\DependencyInjection\ContainerBuilder */ public static $di; }
А дальше нужно провернуть еще пару махинаций, чтобы положить в этой свойство DI-контейнер между инициализацией коханы и выполнением кода контроллера. Для этого разделим наш файл инициализации app_kohana.php на две части, выделив непосредственно инициализацию системы и сам запуск контроллера.
/** app_kohana_init.php */ // тут инициализация фреймворка, включая системные константы и bootstrap /** app_kohana_run.php */ echo Request::factory(TRUE, array(), FALSE) ->execute() ->send_headers(TRUE) ->body(); /** app_kohana.php */ include 'app_kohana_init.php'; include 'app_kohana_run.php';
Модифицируем наш контроллер, проделывая похожие с app_kohana.php операции, но добавляя между инклюдами проброс контейнера
public function kohanaAction() { ob_start(); $kohanaPath = $this->container->getParameter('kernel.root_dir') . '/..'; include $kohanaPath . '/app_kohana_init.php'; \Kohana::$di = $this->container; include $kohanaPath . '/app_kohana_run.php'; $headers = headers_list(); $code = http_response_code(); $response = ob_get_clean(); return new Response($response, $code, $headers); }
После этого мы в старой системе можем использовать DI-контейнер и все объявленные в новой системе сервисы, включая EntityManager и новые модели доктрины.
Напоследок
Плюсы реализации
- Мы сделали первый шаг для дальнейшего развития системы.
- Новая система независима от старой. Весь новый код работает без участия старого
- Минимум потраченного времени
Минусы реализации
- Дополнительные накладные ресурсы на «обертку» во время работы со старой частью системы. Однако, по сравнению с задержками в старой системе, оверхедом (как по памяти, так и по процессору) можно пренебречь.
- Новая система независима от старой. Мы не можем использовать старый код в новой, но это скорее плюс, раз уж мы решились переписывать.
- Приходится поддерживать модели в двух местах.
Спасибо, что дочитали до конца, желаю успехов в рефакторинге, смахните накопившуюся пыль со старого кода!
И простите за ужасные шрифты на диаграммах 🙁
ссылка на оригинал статьи http://habrahabr.ru/post/252405/